package com.lz.ovuola.service.impl;

import java.io.Serializable;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import javax.annotation.Resource;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.lz.ovuola.entity.PhysiologicalBasisDo;
import com.lz.ovuola.entity.PhysiologicalDataDo;
import com.lz.ovuola.entity.PhysiologicalEstimateDataDo;
import com.lz.ovuola.entity.PhysiologicalReferencevDataDo;
import com.lz.ovuola.repository.mysql.impl.PhysiologicalBasisRepository;
import com.lz.ovuola.repository.mysql.impl.PhysiologicalDataRepository;
import com.lz.ovuola.repository.mysql.impl.PhysiologicalEstimateDataRepository;
import com.lz.ovuola.repository.mysql.impl.PhysiologicalReferencevDataRepository;
import com.lz.ovuola.service.PhysiologicalDataService;
import com.lz.ovuola.service.assembler.PhysiologicalDataAssembler;
import com.lz.ovuola.service.assembler.PhysiologicalDataListProListAssembler;
import com.lz.ovuola.util.annotation.TargetDataSource;
import com.lz.ovuola.util.concurrent.ThreadPoolExecutor;
import com.lz.ovuola.util.constant.DataSourceConsts;
import com.lz.ovuola.util.constant.RedisCacheConsts;
import com.lz.ovuola.util.date.DateUtils;
import com.lz.ovuola.util.exception.BusinessException;
import com.lz.ovuola.util.exception.msg.CommonMsgEnum;
import com.lz.ovuola.util.exception.msg.PhysiologicalMsgEnum;
import com.lz.ovuola.util.redis.RedisTemplateRepository;
import com.lz.ovuola.util.response.ResponseDataPackageUtils;
import com.lz.ovuola.util.threadlocal.UserContextHolder;

/**
 * 硬件检测数值计算
 * 
 * @author fz
 *
 */
@Service("physiologicalDataService")
public class PhysiologicalDataServiceImpl implements PhysiologicalDataService {
	@Resource
	private PhysiologicalDataFormula physiologicalDataFormula;

	private static Logger logger = LoggerFactory
			.getLogger(PhysiologicalDataServiceImpl.class);
	@Resource
	private PhysiologicalBasisRepository physiologicalBasisRepository;

	@Resource
	private PhysiologicalEstimateDataRepository physiologicalEstimateDataRepository;

	@Autowired
	private RedisTemplateRepository redisTemplateRepository;

	@Resource
	private PhysiologicalDataRepository physiologicalDataRepository;

	@Resource
	private PhysiologicalReferencevDataRepository physiologicalReferencevDataRepository;

	private final int NEGATIVE_OVU = -7;

	private final int POSITIVE_OVU = 7;
	/**
	 * 激素值数量
	 */
	private final int WAC_NUMBER = 10;

	@TargetDataSource(DataSourceConsts.PRIMARY_DS)
	@Transactional(transactionManager = "transactionManagerDynamic", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = false)
	@Override
	public String saveData(Date detectionTime, Float ch1rh, Float ch1rl,
			Float ch2rh, Float ch2rl, Float ch3r, Float ch1vl, Float ch2vl,
			Integer sick, Integer leedingGums, Integer userId) throws Exception {
		// 校验参数合法性
		validate(detectionTime, ch1rh, ch1rl, ch2rh, ch2rl, ch3r, ch1vl, ch2vl,
				sick, leedingGums);

		PhysiologicalBasisDo physiologicalBasisDo = physiologicalBasisRepository
				.findByUserId(userId);
		if (!ObjectUtils.notEqual(physiologicalBasisDo, null))
			throw new BusinessException(
					PhysiologicalMsgEnum.MEN_IS_UPLOAD.getStatus(),
					PhysiologicalMsgEnum.MEN_IS_UPLOAD.getMsg());
		// 预估排卵日
		Date estimateOvulationTime = null;
		// 预估下次例假开始时间
		Date estimateNextStartTime = null;
		// 更新预估排卵日
		boolean updateEsData = false;
		// 暂未启动公式A
		boolean isFormulaA = false;
		// is首次检测
		boolean isFirst = false;
		// 基准值
		final Float rv = getRv(userId);
		// 激素值
		final Float waterContent = physiologicalDataFormula
				.getWaterContent(ch1rh);
		// 例假开始时间
		final Date startTime = physiologicalBasisDo.getStartTime();
		// 生理周期长度
		final Integer physiologicalLength = physiologicalBasisDo
				.getPhysiologicalLength();

		PhysiologicalEstimateDataDo physiologicalEstimateDataDo = physiologicalEstimateDataRepository
				.findByUserIdAndStartTime(userId,
						physiologicalBasisDo.getStartTime());
		if (!ObjectUtils.notEqual(physiologicalEstimateDataDo, null)) {
			// 首次检测 公式A
			updateEsData = true;
			isFormulaA = true;
			isFirst = true;
			logger.info("------用户{}在本次生理周期内首次检测， 插入到预估最精确排卵日表，采用公式A计算",
					UserContextHolder.get().getUserId());
		} else {
			// 检测日期在ovu之外
			if (!isOvu(physiologicalDataFormula.getEstimateOvuTimeByFormulaA(
					physiologicalLength, startTime), detectionTime)) {
				isFormulaA = true;
				logger.info("------用户{}在本次检测在ovu外，不更新最精确排卵日表，所以采用公式A计算",
						UserContextHolder.get().getUserId());
			} else {
				// 检测日期ovu之内, // 激素值-基准值<阈值
				if (!rvIsGtThreshold(waterContent, userId, rv)) {
					isFormulaA = true;
					logger.info(
							"------用户{}在本次检测在ovu内，但是激素值-基准值<阈值，不更新最精确排卵日表，所以采用公式A计算",
							UserContextHolder.get().getUserId());
				} else {
					// 激素值-基准值>阈值
					estimateOvulationTime = physiologicalDataFormula
							.getEstimateOvuTimeByFormulaB(detectionTime);
					estimateNextStartTime = physiologicalDataFormula
							.getEstimateStartTimeByFormulaB(detectionTime);
					Float wacTemp = 0.00F;
					if (physiologicalEstimateDataDo.getWaterContent() != null)
						wacTemp = physiologicalEstimateDataDo.getWaterContent();
					// 本次激素值>最高激素值
					if (waterContent > wacTemp) {
						updateEsData = true;
						physiologicalEstimateDataDo
								.setWaterContent(waterContent);
						physiologicalEstimateDataDo
								.setFormula(PhysiologicalConst.FORMULA_B);
						physiologicalEstimateDataDo.setUpdateTime(new Date());
						physiologicalEstimateDataDo
								.setEstimateOvulationTime(estimateOvulationTime);
						physiologicalEstimateDataDo
								.setEstimateNextStartTime(estimateNextStartTime);
						logger.info(
								"------用户{}在本次检测在ovu内，但是激素值-基准值>阈值 ,采用公式B计算，并且大于之前最高激素值，"
										+ "所以要更新最精确排卵日表", UserContextHolder
										.get().getUserId());
					} else {
						logger.info(
								"------用户{}在本次检测在ovu内，但是激素值-基准值>阈值 采用公式B计算,但小于之前最高激素值，"
										+ "所以不更新最精确排卵日表", UserContextHolder
										.get().getUserId());
					}
				}
			}
		}
		if (isFormulaA) {
			estimateOvulationTime = physiologicalDataFormula
					.getEstimateOvuTimeByFormulaA(physiologicalLength,
							startTime);
			estimateNextStartTime = physiologicalDataFormula
					.getEstimateStartTimeByFormulaA(physiologicalLength,
							startTime);
			// 首次检测
			if (isFirst)
				physiologicalEstimateDataDo = new PhysiologicalEstimateDataDo(
						userId, startTime, estimateOvulationTime,
						estimateNextStartTime, new Date(), new Date(),
						PhysiologicalConst.FORMULA_A);

		}
		// 易孕指数
		final String pregnancyRateDescription = physiologicalDataFormula
				.getPregnancyRateDescription(detectionTime,
						estimateOvulationTime);
		// 生理周期阶段
		final String physiologicalStage = physiologicalDataFormula
				.getPhysiologicalStage(physiologicalBasisDo.getEndTime(),
						detectionTime, estimateOvulationTime);
		// 排卵描述
		final String eovtDescription = physiologicalDataFormula
				.getEovtDescription(estimateOvulationTime, physiologicalStage,
						detectionTime);
		// 预估数据
		if (updateEsData)
			physiologicalEstimateDataRepository
					.save(physiologicalEstimateDataDo);
		// 运算数据
		final PhysiologicalDataDo physiologicalDataDo = PhysiologicalDataDo
				.custom(userId, ch1rh, ch1rl, ch2rh, ch2rl, ch3r, ch1vl, ch2vl,
						waterContent,
						physiologicalDataFormula.isMen(physiologicalStage),
						sick, leedingGums, startTime,
						physiologicalBasisDo.getEndTime(),
						estimateOvulationTime, estimateNextStartTime,
						eovtDescription, pregnancyRateDescription,
						physiologicalStage,
						physiologicalEstimateDataDo.getFormula(), detectionTime);
		physiologicalDataRepository.save(physiologicalDataDo);
		// 基准值计算
		ThreadPoolExecutor.execute(() -> {
			calculate(userId, waterContent);
		});
		// 成功
		return ResponseDataPackageUtils.success(PhysiologicalDataAssembler
				.getAssembler().toDto(physiologicalDataDo));
	}

	@TargetDataSource(DataSourceConsts.SECONDARY_DS)
	@Transactional(transactionManager = "transactionManagerDynamic", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	@Override
	public String get(Date startTime, Date endTime) throws Exception {
		List<PhysiologicalDataDo> dataDos = physiologicalDataRepository
				.findByDetectionTimeBetweenAndUserId(startTime, endTime,
						UserContextHolder.get().getUserId());
		if (CollectionUtils.isEmpty(dataDos))
			return ResponseDataPackageUtils.success();
		return ResponseDataPackageUtils
				.success(PhysiologicalDataListProListAssembler.getAssembler()
						.toDto(dataDos, startTime, endTime));
	}

	/**
	 * 基准值运算
	 * 
	 * @param userId
	 * @param wt
	 *            激素值
	 */
	@TargetDataSource(DataSourceConsts.PRIMARY_DS)
	@Transactional(transactionManager = "transactionManagerDynamic", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = false)
	protected void calculate(Integer userId, Float wt) {
		// 读库标识
		boolean isReadb = false;
		// 读缓存
		final List<Serializable> rvsTemps = redisTemplateRepository.lrangeAll(
				RedisCacheConsts.OVUOLA_PY_WATER_CONTENT_NAME_SPACE,
				String.valueOf(userId));
		List<Float> rvs = null;
		if (CollectionUtils.isEmpty(rvsTemps)) {
			// 若缓存为空，则读取库
			List<PhysiologicalDataDo> physiologicalDataDos = physiologicalDataRepository
					.findByUserId(userId);
			if (!CollectionUtils.isEmpty(physiologicalDataDos)) {
				rvs = getReturn(physiologicalDataDos, userId);
				isReadb = true;
			}
			logger.info("用户:{}基准值运算中，将库中所有激素值写入缓存", userId);
		} else {
			rvs = get(rvsTemps);
		}
		if (CollectionUtils.isEmpty(rvs))
			rvs = new ArrayList<Float>(1);
		if (!isReadb)
			rvs.add(wt);
		// 新基准值
		float newRv = 0.00F;
		// 1<N<10,取中间值作为基准值
		if (rvs.size() < WAC_NUMBER) {
			Collections.sort(rvs, (first, second) -> first.compareTo(second));
			newRv = rvs.get(rvs.size() / 2);
			logger.info("---用户:{}取中间值作为最新基准值{}", userId, newRv);
		} else {
			// 通过符合正态分布来运算得到最准确的基准值，首先计算期望值
			final float mean = physiologicalDataFormula.getMean(rvs);
			// 计算标准差
			final float standardDeviation = physiologicalDataFormula
					.getStandardDeviation(rvs, mean);
			// 计算剔除后的平均值
			final List<Float> datas = satisfyData(rvs, mean, standardDeviation);
			if (CollectionUtils.isEmpty(datas))
				logger.warn("---用户:{}基准值运算中，激素值所有数据都不符合正态分布，估计用户使用过程中有不当行为",
						userId);
			newRv = physiologicalDataFormula.getMean(datas);
			logger.info(
					"---用户id:{}通过符合正态分布计算得出最新基准值，总共激素值个数:{},其中符合的个数:{},期望:{},标准差:{}",
					userId, rvs.size(), datas.size(), mean, standardDeviation);

		}
		PhysiologicalReferencevDataDo phylrvd = new PhysiologicalReferencevDataDo(
				newRv, null, userId);
		physiologicalReferencevDataRepository.save(phylrvd);
		// 更新激素值缓存
		if (!isReadb) {
			long number = redisTemplateRepository.lpush(
					RedisCacheConsts.OVUOLA_PY_WATER_CONTENT_NAME_SPACE,
					String.valueOf(userId), wt);
			if (number != rvs.size()) {
				logger.error("---用户:{}基准值运算中，往缓存激素值数组添加过程中出现异常", userId);
				throw new BusinessException(
						CommonMsgEnum.SERVER_INTERNAL_ERROR.getStatus(),
						CommonMsgEnum.SERVER_INTERNAL_ERROR.getMsg());
			}
		}
		// 更新基准值缓存
		redisTemplateRepository.set(RedisCacheConsts.OVUOLA_PY_RV_NAME_SPACE,
				String.valueOf(userId), phylrvd);
		logger.info("---用户:{}基准值运算完成，新的基准值是：{}", userId, newRv);
	}

	/**
	 * 转换
	 * 
	 * @param rvsTemps
	 * @return
	 */
	private List<Float> get(List<Serializable> rvsTemps) {
		List<Float> rvs = new ArrayList<Float>(rvsTemps.size());
		for (Serializable temp : rvsTemps) {
			if (temp instanceof Float) {
				rvs.add((Float) temp);
			} else {
				logger.error("---用户:{}基准值运算中出现异常", UserContextHolder.get()
						.getUserId());
				throw new BusinessException(
						CommonMsgEnum.SERVER_INTERNAL_ERROR.getStatus(),
						CommonMsgEnum.SERVER_INTERNAL_ERROR.getMsg());
			}
		}
		return rvs;
	}

	/**
	 * 将库所有激素值写入缓存
	 * 
	 * @param physiologicalDataDos
	 * @return
	 */
	private List<Float> getReturn(
			List<PhysiologicalDataDo> physiologicalDataDos, Integer userId) {
		final List<Float> rvs = new ArrayList<Float>(
				physiologicalDataDos.size());
		final List<Serializable> rvs2 = new ArrayList<Serializable>(
				physiologicalDataDos.size());
		for (PhysiologicalDataDo physiologicalDataDo : physiologicalDataDos) {
			rvs.add(physiologicalDataDo.getWaterContent());
			rvs2.add(physiologicalDataDo.getWaterContent());
		}
		// 写入缓存
		long number = redisTemplateRepository.lpushAllByList(
				RedisCacheConsts.OVUOLA_PY_WATER_CONTENT_NAME_SPACE,
				String.valueOf(userId), rvs2);
		if (number != physiologicalDataDos.size()) {
			logger.error("---用户:{}基准值运算中，将库中所有激素值写入缓存中出现错误", UserContextHolder
					.get().getUserId());
			throw new BusinessException(
					CommonMsgEnum.SERVER_INTERNAL_ERROR.getStatus(),
					CommonMsgEnum.SERVER_INTERNAL_ERROR.getMsg());
		}
		return rvs;

	}

	/**
	 * 剔除不满足正态分布的激素值
	 * 
	 * @param rvs
	 * @param mean
	 * @param standardDeviation
	 * @return
	 */
	private List<Float> satisfyData(List<Float> rvs, float mean,
			float standardDeviation) {
		final float satisfyBegin = mean - standardDeviation;
		final float satisfyEnd = mean + standardDeviation;
		List<Float> rvsTemp = new ArrayList<Float>();
		for (Float rv : rvs) {
			if (rv >= satisfyBegin && rv <= satisfyEnd)
				rvsTemp.add(rv);
		}
		return rvsTemp;

	}

	/**
	 * 激素值-基准值>阈值 ?
	 * 
	 * @param waterContent
	 * @param userId
	 * @return
	 */
	private boolean rvIsGtThreshold(Float waterContent, Integer userId, Float rv) {
		if (waterContent - rv >= getThreshold())
			return true;
		else
			return false;
	}

	/**
	 * 判断是否在ovu
	 * 
	 * @param estimateOvulationTime
	 *            14日法算
	 * @param detectionTime
	 * @return
	 */
	private boolean isOvu(Date estimateOvulationTime, Date detectionTime) {
		Date ovuStartTime = DateUtils.getAfterSeveralUnits(
				estimateOvulationTime, NEGATIVE_OVU, ChronoUnit.DAYS);
		Date ovuEndTime = DateUtils.getAfterSeveralUnits(estimateOvulationTime,
				POSITIVE_OVU, ChronoUnit.DAYS);
		if ((DateUtils.gt(detectionTime, ovuStartTime) && DateUtils.gt(
				ovuEndTime, detectionTime))
				|| DateUtils.eq(detectionTime, ovuStartTime)
				|| DateUtils.eq(ovuEndTime, detectionTime))
			return true;
		else
			return false;
	}

	/**
	 * 最近一次基准值查询
	 * 
	 * @param userId
	 * @return
	 */
	Float getRv(Integer userId) {
		final PhysiologicalReferencevDataDo physiologicalReferencevData = (PhysiologicalReferencevDataDo) redisTemplateRepository
				.get(RedisCacheConsts.OVUOLA_PY_RV_NAME_SPACE,
						String.valueOf(userId));
		Float referenceValue = 0.00F;
		if (physiologicalReferencevData != null) {
			referenceValue = physiologicalReferencevData.getReferenceValue();
		} else {
			// 查库
			List<PhysiologicalReferencevDataDo> dataDos = physiologicalReferencevDataRepository
					.findByUserIdOrderByCreateTimeDesc(userId);
			if (!CollectionUtils.isEmpty(dataDos))
				referenceValue = dataDos.get(0).getReferenceValue();
		}

		return referenceValue;

	}

	/**
	 * 激增阈值 暂定，后期会根据用户id来获取
	 * 
	 * @param userId
	 * @return
	 */
	private Float getThreshold() {
		return PhysiologicalConst.THRE_SHOLD;
	}

	/**
	 * 参数合法性校验
	 * 
	 * @param detectionTime
	 * @param ch1rh
	 * @param ch1rl
	 * @param ch2rh
	 * @param ch2rl
	 * @param ch3r
	 * @param ch1vl
	 * @param ch2vl
	 * @param sick
	 * @param leedingGums
	 */
	private void validate(Date detectionTime, Float ch1rh, Float ch1rl,
			Float ch2rh, Float ch2rl, Float ch3r, Float ch1vl, Float ch2vl,
			Integer sick, Integer leedingGums) {
		// if (DateUtils.gt(detectionTime, new Date()))
		// throw new BusinessException(
		// PhysiologicalMsgEnum.DET_TIME_IS_GT_NOW.getStatus(),
		// PhysiologicalMsgEnum.DET_TIME_IS_GT_NOW.getMsg());
		if (ch1rh <= 0 || ch1rl <= 0 || ch2rh < 0 || ch2rl <= 0 || ch3r <= 0
				|| ch1vl <= 0 || ch2vl <= 0)
			logger.warn(
					"用户id:{}测出异常数据------ch1rh:{},ch1rl:{},ch2rh:{},ch2rl:{},ch3r:{} ,ch1vl:{} ,ch2vl:{}------",
					UserContextHolder.get().getUserId(), ch1rh, ch1rl, ch2rh,
					ch2rl, ch3r, ch1vl, ch2vl);

		if ((sick == 1 || sick == 2) && (leedingGums == 1 || leedingGums == 2)) {
		} else {
			throw new BusinessException(
					CommonMsgEnum.SERVER_INTERNAL_ERROR.getStatus(),
					CommonMsgEnum.SERVER_INTERNAL_ERROR.getMsg());
		}
	}

}
